import * as cs from './CSharpAst';
import * as ts from 'typescript';
import AstPrinterBase from '../AstPrinterBase';

export default class CSharpAstPrinter extends AstPrinterBase {
    private _keywords: Set<string> = new Set<string>([
        'abstract',
        'as',
        'base',
        'bool',
        'break',
        'byte',
        'case',
        'catch',
        'char',
        'checked',
        'class',
        'const',
        'continue',
        'decimal',
        'default',
        'delegate',
        'do',
        'double',
        'else',
        'enum',
        'event',
        'explicit',
        'extern',
        'false',
        'finally',
        'fixed',
        'float',
        'for',
        'foreach',
        'goto',
        'if',
        'implicit',
        'in',
        'int',
        'interface',
        'internal',
        'is',
        'lock',
        'long',
        'namespace',
        'new',
        'null',
        'object',
        'operator',
        'out',
        'override',
        'params',
        'private',
        'protected',
        'public',
        'readonly',
        'ref',
        'return',
        'sbyte',
        'sealed',
        'short',
        'sizeof',
        'stackalloc',
        'static',
        'string',
        'struct',
        'switch',
        'this',
        'throw',
        'true',
        'try',
        'typeof',
        'uint',
        'ulong',
        'unchecked',
        'unsafe',
        'ushort',
        'using',
        'virtual',
        'void',
        'volatile',
        'while'
    ]);

    protected override escapeIdentifier(identifier: string): string {
        if (this._keywords.has(identifier)) {
            return `@${identifier}`;
        }
        return identifier;
    }

    protected writeSourceFile(sourceFile: cs.SourceFile) {
        this.writeLine('// <auto-generated>');
        this.writeLine('// This code was auto-generated.');
        this.writeLine('// Changes to this file may cause incorrect behavior and will be lost if');
        this.writeLine('// the code is regenerated.');
        this.writeLine('// </auto-generated>');
        this.writeLine('#nullable enable annotations');
        this.writeLine();
        for (const using of sourceFile.usings) {
            this.writeUsing(using);
        }
        if (sourceFile.usings.length > 0) {
            this.writeLine();
        }
        this.writeNamespace(sourceFile.namespace);
    }

    protected writeNamespace(namespace: cs.NamespaceDeclaration) {
        this.writeLine(`namespace ${namespace.namespace}`);
        this.beginBlock();
        this.writeNamespaceMembers(namespace.declarations);
        this.endBlock();
    }

    protected writeDocumentation(d: cs.DocumentedElement) {
        if (d.documentation) {
            this.writeLine('/// <summary>');
            this.writeDocumentationLines(d.documentation, true);
            this.writeLine('/// </summary>');
        }
    }

    protected writeDocumentationLines(documentation: string, multiLine: boolean) {
        const lines = documentation.split('\n');
        if (lines.length > 1 || multiLine) {
            if (!this._isStartOfLine) {
                this.writeLine();
            }
            for (const line of lines) {
                this.writeLine(`/// ${this.escapeXmlDoc(line)}`);
            }
        } else if (lines.length === 1) {
            if (this._isStartOfLine) {
                this.writeLine(`/// ${this.escapeXmlDoc(lines[0])}`);
            } else {
                this.write(this.escapeXmlDoc(lines[0]));
            }
        }
    }
    protected escapeXmlDoc(s: string): string {
        return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
    }

    protected writeParameter(p: cs.ParameterDeclaration) {
        if (p.params) {
            this.write('params ');
        }
        if (p.type) {
            this.writeType(p.type, false, p.params);
        }
        this.write(' ');
        this.writeIdentifier(p.name);

        if (p.initializer) {
            this.write(' = ');
            this.writeExpression(p.initializer);
        } else if (p.type && p.isOptional) {
            this.write(' = default');
        }
    }

    protected writeDelegateDeclaration(d: cs.DelegateDeclaration) {
        this.writeDocumentation(d);
        this.writeAttributes(d);
        this.writeVisibility(d.visibility);

        this.write('delegate ');
        this.writeType(d.returnType);
        this.write(` ${d.name}`);
        this.writeTypeParameters(d.typeParameters);
        this.writeParameters(d.parameters);
        this.writeTypeParameterConstraints(d.typeParameters);

        this.writeSemicolon();
    }

    protected writeInterfaceDeclaration(d: cs.InterfaceDeclaration) {
        this.writeDocumentation(d);
        this.writeAttributes(d);
        this.writeVisibility(d.visibility);

        if (d.partial) {
            this.write('partial ');
        }

        this.write(`interface ${d.name}`);
        this.writeTypeParameters(d.typeParameters);

        if (d.interfaces && d.interfaces.length > 0) {
            this.write(': ');
            this.writeCommaSeparated(d.interfaces, i => this.writeType(i));
        }

        this.writeTypeParameterConstraints(d.typeParameters);
        this.writeLine();
        this.beginBlock();

        for (const m of d.members) {
            this.writeMember(m);
        }

        this.endBlock();
    }

    protected writeEnumDeclaration(d: cs.EnumDeclaration) {
        this.writeDocumentation(d);
        this.writeAttributes(d);
        this.writeVisibility(d.visibility);
        this.write(`enum ${d.name}`);
        this.writeLine();
        this.beginBlock();

        for (const m of d.members) {
            this.writeEnumMember(m);
        }

        this.endBlock();
    }

    protected writeEnumMember(m: cs.EnumMember): void {
        this.writeDocumentation(m);
        this.writeAttributes(m);
        this.write(m.name);
        if (m.initializer) {
            this.write(' = ');
            this.writeExpression(m.initializer);
        }
        this.writeLine(',');
    }

    protected writeClassDeclaration(d: cs.ClassDeclaration) {
        this.writeDocumentation(d);
        this.writeAttributes(d);
        this.writeVisibility(d.visibility);

        if (d.partial) {
            this.write('partial ');
        }

        if (d.isAbstract) {
            this.write('abstract ');
        }

        this.write(`class ${d.name}`);
        this.writeTypeParameters(d.typeParameters);

        if (d.baseClass) {
            this.write(': ');
            this.writeType(d.baseClass);
        }

        if (d.interfaces && d.interfaces.length > 0) {
            if (d.baseClass) {
                this.write(', ');
            } else {
                this.write(': ');
            }

            this.writeCommaSeparated(d.interfaces, i => this.writeType(i));
        }
        this.writeTypeParameterConstraints(d.typeParameters);

        this.writeLine();

        this.beginBlock();

        let hasConstuctor = false;
        for (const m of d.members) {
            this.writeMember(m);
            if (cs.isConstructorDeclaration(m) && !m.isStatic) {
                hasConstuctor = true;
            }
        }

        if (d.baseClass && !hasConstuctor) {
            let baseClass: cs.TypeReferenceType | undefined = d;
            let constructorDeclaration: cs.ConstructorDeclaration | undefined = undefined;
            while (baseClass && !constructorDeclaration) {
                if (typeof baseClass === 'string') {
                    constructorDeclaration = undefined;
                    break;
                }

                if (cs.isClassDeclaration(baseClass)) {
                    constructorDeclaration = baseClass.members.find(m =>
                        cs.isConstructorDeclaration(m)
                    ) as cs.ConstructorDeclaration;
                    if (constructorDeclaration) {
                        break;
                    }

                    baseClass =
                        baseClass.baseClass && cs.isTypeReference(baseClass.baseClass)
                            ? baseClass.baseClass.reference
                            : undefined;
                } else {
                    constructorDeclaration = undefined;
                    break;
                }
            }

            if (constructorDeclaration) {
                const defaultConstructor = {
                    parent: d,
                    name: '',
                    nodeType: cs.SyntaxKind.ConstructorDeclaration,
                    isStatic: false,
                    parameters: [],
                    visibility: cs.Visibility.Public,
                    body: {
                        parent: null,
                        nodeType: cs.SyntaxKind.Block,
                        statements: [],
                        tsNode: d.tsNode
                    } as cs.Block,
                    tsNode: d.tsNode,
                    baseConstructorArguments: []
                } as cs.ConstructorDeclaration;
                defaultConstructor.body!.parent = defaultConstructor;
                defaultConstructor.parameters = constructorDeclaration.parameters;
                defaultConstructor.baseConstructorArguments = constructorDeclaration.parameters.map(
                    p =>
                        ({
                            parent: defaultConstructor,
                            nodeType: cs.SyntaxKind.Identifier,
                            text: p.name,
                            tsNode: defaultConstructor.tsNode
                        }) as cs.Identifier
                );
                this.writeMember(defaultConstructor);
            }
        }

        this.endBlock();
    }

    protected writeAttribute(a: cs.Attribute): void {
        this.write('[');
        this.writeType(a.type);
        if (a.arguments && a.arguments.length > 0) {
            this.write('(');
            this.writeCommaSeparated(a.arguments!, x => this.writeExpression(x));
            this.write(')');
        }
        this.writeLine(']');
    }

    protected writeMethodDeclaration(d: cs.MethodDeclaration) {
        this.writeDocumentation(d);
        this.writeParameterDocumentation(d);

        this.writeAttributes(d);
        this.writeVisibility(d.visibility);

        if (d.isStatic) {
            this.write('static ');
        }

        if (d.isAsync) {
            this.write('async ');
        }

        if (d.isAbstract) {
            this.write('abstract ');
        } else if (d.isVirtual) {
            this.write('virtual ');
        } else if (d.isOverride) {
            this.write('override ');
        }

        this.writeType(d.returnType);

        this.write(` ${d.name}`);
        this.writeTypeParameters(d.typeParameters);
        this.writeParameters(d.parameters);
        this.writeTypeParameterConstraints(d.typeParameters);

        this.writeBody(d.body);
        this.writeLine();

        if (this._isGetEnumerator(d)) {
            this.write('System.Collections.Generic.IEnumerator<');
            this.writeType((d.returnType as cs.TypeReference).typeArguments![0]);
            this.write('> System.Collections.Generic.IEnumerable<');
            this.writeType((d.returnType as cs.TypeReference).typeArguments![0]);
            this.writeLine('>.GetEnumerator()');
            this.beginBlock();
            this.writeLine('return GetEnumerator();');
            this.endBlock();
            this.writeLine('System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() ');
            this.beginBlock();
            this.writeLine('return GetEnumerator();');
            this.endBlock();
        }
    }

    private _isGetEnumerator(d: cs.MethodDeclaration) {
        return (
            d.name === 'GetEnumerator' &&
            cs.isTypeReference(d.returnType) &&
            d.returnType.typeArguments &&
            d.returnType.typeArguments.length === 1
        );
    }

    protected writeParameterDocumentation(d: cs.MethodDeclaration) {
        for (const p of d.parameters) {
            if (p.documentation) {
                this.write(`/// <param name="${p.name}">`);
                this.writeDocumentationLines(p.documentation, false);
                if (this._isStartOfLine) {
                    this.write('/// ');
                }
                this.writeLine('</param>');
            }
        }
    }

    protected writeBody(body: cs.Expression | cs.Block | undefined) {
        if (body) {
            if (cs.isBlock(body)) {
                this.writeLine();
                this.writeBlock(body);
            } else {
                this.write(' => ');
                this.writeExpression(body as cs.Expression);
                this.writeLine(';');
            }
        } else {
            this.writeLine(';');
        }
    }

    protected writeConstructorDeclaration(d: cs.ConstructorDeclaration) {
        this.writeDocumentation(d);
        this.writeAttributes(d);
        this.writeVisibility(d.visibility);
        if (d.isStatic) {
            this.write('static ');
        }
        this.write(`${(d.parent as cs.ClassDeclaration).name}`);
        this.writeParameters(d.parameters);

        if (d.baseConstructorArguments) {
            this.writeLine();
            this._indent++;
            this.write(': base (');
            this.writeCommaSeparated(d.baseConstructorArguments, e => this.writeExpression(e));
            this.write(')');
            this._indent--;
        }

        this.writeBody(d.body);
    }

    protected writePropertyDeclaration(d: cs.PropertyDeclaration) {
        this.writeDocumentation(d);
        this.writeAttributes(d);
        this.writeVisibility(d.visibility);

        const writeAsField = this.writePropertyAsField(d);

        if (writeAsField && this.canBeConstant(d)) {
            this.write('const ');
        } else {
            if (d.isStatic) {
                this.write('static ');
            }

            if (d.isAbstract) {
                this.write('abstract ');
            } else if (d.isVirtual) {
                this.write('virtual ');
            } else if (d.isOverride) {
                this.write('override ');
            }
        }

        this.writeType(d.type);
        this.write(` ${d.name}`);

        if (!writeAsField) {
            this.writeLine();
            this.beginBlock();

            if (d.getAccessor) {
                this.writePropertyAccessor(d.getAccessor);
            }

            if (d.setAccessor) {
                this.writePropertyAccessor(d.setAccessor);
            }

            this.endBlock();
        }

        if (d.initializer) {
            this.write(' = ');
            this.writeExpression(d.initializer);
            this.writeLine(';');
        } else if (writeAsField) {
            this.writeLine(';');
        }
    }

    protected writePropertyAccessor(accessor: cs.PropertyAccessorDeclaration) {
        this.write(accessor.keyword);
        this.writeBody(accessor.body);
    }

    protected writeFieldDeclarat1on(d: cs.FieldDeclaration) {
        this.writeDocumentation(d);
        this.writeAttributes(d);
        this.writeVisibility(d.visibility);

        if (this._context.isConst(d)) {
            this.write('const ');
        } else {
            if (d.isStatic) {
                this.write('static ');
            }

            if (d.isReadonly) {
                this.write('readonly ');
            }
        }

        this.writeType(d.type);
        this.write(` ${d.name}`);
        if (d.initializer) {
            this.write(' = ');
            this.writeExpression(d.initializer);
        }
        this.writeLine(';');
    }

    protected writeType(
        type: cs.TypeNode,
        forNew: boolean = false,
        asNativeArray: boolean = false,
        forTypeConstraint: boolean = false
    ) {
        switch (type.nodeType) {
            case cs.SyntaxKind.PrimitiveTypeNode:
                if (forTypeConstraint) {
                    switch ((type as cs.PrimitiveTypeNode).type) {
                        case cs.PrimitiveType.Bool:
                        case cs.PrimitiveType.Int:
                        case cs.PrimitiveType.Double:
                            this.write('struct');
                            break;
                        case cs.PrimitiveType.Object:
                        case cs.PrimitiveType.String:
                        case cs.PrimitiveType.Void:
                            this.write('class');
                            break;
                    }
                } else {
                    switch ((type as cs.PrimitiveTypeNode).type) {
                        case cs.PrimitiveType.Bool:
                            this.write('bool');
                            break;
                        case cs.PrimitiveType.Double:
                            this.write('double');
                            break;
                        case cs.PrimitiveType.Int:
                            this.write('int');
                            break;
                        case cs.PrimitiveType.Object:
                            this.write('object');
                            break;
                        case cs.PrimitiveType.String:
                            this.write('string');
                            break;
                        case cs.PrimitiveType.Void:
                            this.write('void');
                            break;
                        case cs.PrimitiveType.Var:
                            this.write('var');
                            break;
                        case cs.PrimitiveType.Long:
                            this.write('long');
                            break;
                    }
                }

                break;
            case cs.SyntaxKind.ArrayTypeNode:
                const arrayType = type as cs.ArrayTypeNode;
                if (asNativeArray) {
                    this.writeType(arrayType.elementType);
                    this.write('[]');
                } else {
                    const isDynamicArray =
                        cs.isPrimitiveTypeNode(arrayType.elementType) &&
                        arrayType.elementType.type === cs.PrimitiveType.Object;
                    if (isDynamicArray && !forNew) {
                        this.write('System.Collections.IList');
                    } else {
                        if (forNew) {
                            this.write('AlphaTab.Collections.List<');
                        } else {
                            this.write('System.Collections.Generic.IList<');
                        }
                        this.writeType(arrayType.elementType);
                        this.write('>');
                    }
                }

                break;
            case cs.SyntaxKind.MapTypeNode:
                const mapType = type as cs.MapTypeNode;
                if (!mapType.keyType && !mapType.valueType) {
                    this.write('System.Collections.IDictionary');
                } else {
                    if (!mapType.valueIsValueType) {
                        if (forNew) {
                            this.write('AlphaTab.Collections.Map<');
                        } else {
                            this.write('AlphaTab.Collections.IMap<');
                        }
                    } else {
                        if (forNew) {
                            this.write('AlphaTab.Collections.ValueTypeMap<');
                        } else {
                            this.write('AlphaTab.Collections.IValueTypeMap<');
                        }
                    }
                    this.writeType(mapType.keyType!);
                    this.write(', ');
                    this.writeType(mapType.valueType!);
                    this.write('>');
                }

                break;
            case cs.SyntaxKind.ArrayTupleNode:
                const arrayTupleType = type as cs.ArrayTupleNode;
                this.write('AlphaTab.Core.ArrayTuple<');
                this.writeCommaSeparated(arrayTupleType.types, p => this.writeType(p));
                this.write('>');
                break;
            case cs.SyntaxKind.FunctionTypeNode:
                const functionType = type as cs.FunctionTypeNode;
                if (
                    cs.isPrimitiveTypeNode(functionType.returnType) &&
                    functionType.returnType.type === cs.PrimitiveType.Void
                ) {
                    this.write('System.Action');
                    if (functionType.parameterTypes.length > 0) {
                        this.write('<');
                        this.writeCommaSeparated(functionType.parameterTypes, p => this.writeType(p));
                        this.write('>');
                    }
                } else {
                    this.write('System.Func');
                    this.write('<');
                    if (functionType.parameterTypes.length > 0) {
                        this.writeCommaSeparated(functionType.parameterTypes, p => this.writeType(p));
                        this.write(', ');
                        this.writeType(functionType.returnType);
                    } else {
                        this.writeType(functionType.returnType);
                    }
                    this.write('>');
                }
                break;
            case cs.SyntaxKind.TypeReference:
                const typeReference = type as cs.TypeReference;
                let isAsyncVoid = false;

                const targetType = (type as cs.TypeReference).reference;
                let typeArguments = typeReference.typeArguments;
                if (typeof targetType === 'string') {
                    if (forNew && targetType === this._context.makeIterableType() && typeArguments) {
                        this.writeType(typeArguments[0]);
                        this.write('[]');
                        typeArguments = undefined;
                    } else {
                        this.write(targetType);
                    }
                } else {
                    if (typeReference.isAsync) {
                        this.write('System.Threading.Tasks.Task');
                        if (!cs.isPrimitiveTypeNode(targetType) || targetType.type !== cs.PrimitiveType.Void) {
                            this.write('<');
                            this.writeType(targetType, forNew);
                        } else {
                            isAsyncVoid = true;
                        }
                    } else {
                        this.writeType(targetType, forNew);
                    }
                }

                if (!isAsyncVoid) {
                    if (typeArguments && typeArguments.length > 0) {
                        this.write('<');
                        this.writeCommaSeparated(typeArguments, p => this.writeType(p));
                        this.write('>');
                    }

                    if (typeReference.isAsync) {
                        this.write('>');
                    }
                }

                break;
            case cs.SyntaxKind.ClassDeclaration:
            case cs.SyntaxKind.InterfaceDeclaration:
            case cs.SyntaxKind.EnumDeclaration:
            case cs.SyntaxKind.DelegateDeclaration:
                this.write(this._context.getFullName(type as cs.NamedTypeDeclaration));
                break;
            case cs.SyntaxKind.UsingDeclaration:
                this.write((type as cs.UsingDeclaration).name);
                break;
            case cs.SyntaxKind.TypeParameterDeclaration:
                this.write((type as cs.TypeParameterDeclaration).name);
                break;
            case cs.SyntaxKind.EnumMember:
                this.write(this._context.getFullName((type as cs.EnumMember).parent as cs.NamedTypeDeclaration));
                break;
            default:
                this.write(`TODO: ${cs.SyntaxKind[type.nodeType]}`);
                break;
        }
        if (type.isNullable && !forNew && !forTypeConstraint) {
            this.write('?');
        }
    }

    protected writeTypeOfExpression(expr: cs.TypeOfExpression) {
        this.write('typeof');

        this.write('(');
        if (expr.expression) {
            this.writeExpression(expr.expression);
        } else if (expr.type) {
            this.writeType(expr.type);
        }
        this.write(')');
    }

    protected writePrefixUnaryExpression(expr: cs.PrefixUnaryExpression) {
        this.write(expr.operator);
        this.writeExpression(expr.operand);
    }

    protected writeBaseLiteralExpression(_expr: cs.BaseLiteralExpression) {
        this.write('base');
    }

    protected writeAwaitExpression(expr: cs.AwaitExpression) {
        this.write('await ');
        this.writeExpression(expr.expression);
    }

    protected writeBinaryExpression(expr: cs.BinaryExpression) {
        this.writeExpression(expr.left);
        this.write(' ');
        this.write(expr.operator);
        this.write(' ');
        this.writeExpression(expr.right);
    }

    protected writeConditionalExpression(expr: cs.ConditionalExpression) {
        this.writeExpression(expr.condition);
        this.write(' ? ');
        this.writeExpression(expr.whenTrue);
        this.write(' : ');
        this.writeExpression(expr.whenFalse);
    }

    protected writeLambdaExpression(expr: cs.LambdaExpression) {
        this.write('(');
        this.writeCommaSeparated(expr.parameters, p => this.writeParameter(p));
        this.write(') => ');
        if (cs.isBlock(expr.body)) {
            this.writeBlock(expr.body);
        } else {
            this.writeExpression(expr.body);
        }
    }

    protected writeNumericLiteral(expr: cs.NumericLiteral) {
        this.write(expr.value);
    }

    protected writeStringTemplateExpression(expr: cs.StringTemplateExpression) {
        this.write('string.Format(System.Globalization.CultureInfo.InvariantCulture, @"');
        const exprs: cs.Expression[] = [];
        for (const c of expr.chunks) {
            if (cs.isStringLiteral(c)) {
                const escapedText = c.text.replaceAll('"', '""').replaceAll('{', '{{').replaceAll('}', '}}');
                this.write(escapedText);
            } else {
                this.write(`{${exprs.length}}`);
                exprs.push(c as cs.Expression);
            }
        }
        this.write('"');
        for (const expr of exprs) {
            this.write(', ');

            if (cs.isStringLiteral(expr)) {
                this.writeExpression(expr);
            } else {
                this.write('(');
                this.writeExpression(expr);
                this.write(').ToTemplate()');
            }
        }
        this.write(')');
    }

    protected writeArrayCreationExpression(expr: cs.ArrayCreationExpression) {
        if (expr.type) {
            this.write('new ');
            this.writeType(expr.type, true);
            if (expr.values) {
                if (expr.values.length > 0) {
                    this.writeLine('{');
                    this._indent++;
                    this.writeCommaSeparated(expr.values, v => {
                        if (expr.values!.length > 10) {
                            this.writeLine();
                        }
                        this.writeExpression(v);
                    });
                    this._indent--;
                    this.writeLine('}');
                } else {
                    this.writeLine('()');
                }
            } else {
                this.write('[');
                this.writeExpression(expr.sizeExpression!);
                this.write(']');
            }
        } else if (expr.values && expr.values.length > 0) {
            this.write('AlphaTab.Core.TypeHelper.CreateList(');
            this.writeCommaSeparated(expr.values, v => {
                if (expr.values!.length > 10) {
                    this.writeLine();
                }
                this.writeExpression(v);
            });
            this.write(')');
        } else {
            this._context.addCsNodeDiagnostics(expr, 'Unknown array type', ts.DiagnosticCategory.Error);
        }
    }

    protected writeMemberAccessExpression(expr: cs.MemberAccessExpression) {
        const isEmpty = cs.isIdentifier(expr.expression) && expr.expression.text.length === 0;
        if (!isEmpty) {
            this.writeExpression(expr.expression);
            this.write(expr.nullSafe ? '?.' : '.');
        }
        const name = this._context.getSymbolName(expr) ?? expr.member;
        this.writeIdentifier(name);
    }

    protected writeElementAccessExpression(expr: cs.ElementAccessExpression) {
        this.writeExpression(expr.expression);
        if (expr.nullSafe) {
            this.write('?');
        }
        this.write('[');
        this.writeExpression(expr.argumentExpression);
        this.write(']');
    }

    protected writeNewExpression(expr: cs.NewExpression) {
        this.write('new ');
        this.writeType(expr.type, true);
        this.write('(');
        this.writeCommaSeparated(expr.arguments, a => this.writeExpression(a));
        this.write(')');
    }

    protected writeCastExpression(expr: cs.CastExpression) {
        this.write('(');
        this.writeType(expr.type);
        this.write(')');
        this.writeExpression(expr.expression);
    }

    protected writeNonNullExpression(expr: cs.NonNullExpression) {
        this.writeExpression(expr.expression);
        if (!cs.isNonNullExpression(expr.expression)) {
            this.write('!');
        }
    }

    private _currentCatchClauseIdentifier: string[] = [];

    protected writeCatchClause(c: cs.CatchClause): void {
        if (c.variableDeclaration) {
            this.write('catch (');
            this.writeType(c.variableDeclaration.type);
            this.write(' ');
            this.write(this.escapeIdentifier(c.variableDeclaration.name));
            this._currentCatchClauseIdentifier.push(c.variableDeclaration.name);
            this.writeLine(')');
            this.writeBlock(c.block);
            this._currentCatchClauseIdentifier.pop();
        } else {
            this.write('catch');
            this.writeBlock(c.block);
        }
    }

    protected writeSwitchStatement(s: cs.SwitchStatement) {
        this.write('switch (');
        this.writeExpression(s.expression);
        this.writeLine(')');
        this.beginBlock();

        for (const c of s.caseClauses) {
            if (cs.isDefaultClause(c)) {
                this.writeDefaultClause(c);
            } else if (cs.isCaseClause(c)) {
                this.writeCaseClause(c);
            }
        }

        this.endBlock();
    }

    protected writeCaseClause(c: cs.CaseClause) {
        this.write('case ');
        this.writeExpression(c.expression);
        this.writeLine(':');
        this._indent++;

        for (const s of c.statements) {
            this.writeStatement(s);
        }
        this._indent--;
    }

    protected writeDefaultClause(c: cs.DefaultClause) {
        this.writeLine('default:');
        this._indent++;
        for (const s of c.statements) {
            this.writeStatement(s);
        }
        this._indent--;
    }

    protected writeForEachStatement(s: cs.ForEachStatement) {
        this.write('foreach (');
        if (cs.isVariableDeclarationList(s.initializer)) {
            this.writeVariableDeclarationList(s.initializer);
        } else {
            this.writeExpression(s.initializer as cs.Expression);
        }
        this.write(' in ');
        this.writeExpression(s.expression);
        this.writeLine(')');

        if (cs.isBlock(s.statement)) {
            this.writeStatement(s.statement);
        } else {
            this._indent++;
            this.writeStatement(s.statement);
            this._indent--;
        }
    }

    protected writeForStatement(s: cs.ForStatement) {
        this.write('for (');
        if (s.initializer) {
            if (cs.isVariableDeclarationList(s.initializer)) {
                this.writeVariableDeclarationList(s.initializer);
            } else {
                this.writeExpression(s.initializer as cs.Expression);
            }
        }
        this.write(';');

        if (s.condition) {
            this.writeExpression(s.condition);
        }
        this.write(';');

        if (s.incrementor) {
            this.writeExpression(s.incrementor);
        }
        this.writeLine(')');

        if (cs.isBlock(s.statement)) {
            this.writeStatement(s.statement);
        } else {
            this._indent++;
            this.writeStatement(s.statement);
            this._indent--;
        }
    }

    protected writeVariableStatement(s: cs.VariableStatement) {
        switch (s.variableStatementKind) {
            case cs.VariableStatementKind.Normal:
            case cs.VariableStatementKind.Const:
                // nothing special in c#
                break;
            case cs.VariableStatementKind.Using:
                this.write('using ');
                break;
            case cs.VariableStatementKind.AwaitUsing:
                this.write('await using ');
                break;
        }
        this.writeVariableDeclarationList(s.declarationList);
        this.writeSemicolon();
    }

    protected writeVariableDeclarationList(declarationList: cs.VariableDeclarationList) {
        this.writeType(declarationList.declarations[0].type);

        declarationList.declarations.forEach((d, i) => {
            if (i === 0) {
                this.write(' ');
            } else {
                this.write(', ');
            }

            if (d.deconstructNames) {
                this.write('(');
                d.deconstructNames.forEach((v, i) => {
                    if (i > 0) {
                        this.write(', ');
                    }
                    this.write(this.escapeIdentifier(v));
                });
                this.write(')');
            } else {
                this.write(this.escapeIdentifier(d.name));
            }

            if (d.initializer) {
                this.write(' = ');
                this.writeExpression(d.initializer);
            }
        });
    }

    protected override writeDeconstructDeclaration(expr: cs.DeconstructDeclaration) {
        this.write('(');
        expr.names.forEach((v, i) => {
            if (i > 0) {
                this.write(', ');
            }
            this.write(this.escapeIdentifier(v));
        });
        this.write(')');
    }

    protected writeBlock(b: cs.Block) {
        this.beginBlock();
        for (const s of b.statements) {
            this.writeStatement(s);
        }
        this.endBlock();
    }

    protected writeUsing(using: cs.UsingDeclaration) {
         if (using.skipEmit) {
            return;
        }

        this.write(`using ${using.name}`);
        if (using.alias) {
            this.write(` = `);
            this.writeType(using.alias, true);
        }
        this.writeLine(';');
    }

    protected writeLocalFunction(expr: cs.LocalFunctionDeclaration) {
        this.writeType(expr.returnType);
        this.write(` ${expr.name}`);
        this.writeParameters(expr.parameters);
        this.writeBlock(expr.body);
    }

    protected writeYieldExpression(expr: cs.YieldExpression) {
        this.write('yield ');
        if (expr.expression) {
            this.write('return ');
            this.writeExpression(expr.expression);
        } else {
            this.write('break');
        }
    }

    protected override writeLabeledExpression(expr: cs.LabeledExpression) {
        this.write(expr.label);
        this.write(': ');
        this.writeExpression(expr.expression);
    }

    protected override writeThrowStatement(s: cs.ThrowStatement) {
        this.write('throw');
        const currentException = this._currentCatchClauseIdentifier[this._currentCatchClauseIdentifier.length -1];
        if (s.expression && (!cs.isIdentifier(s.expression) || s.expression.text !== currentException)) {
            this.write(' ');
            this.writeExpression(s.expression);
        }
        this.writeSemicolon();
    }
}
